﻿namespace Ochkin.SquaredSoccer.Base
    open System

    module internal Utils =
        let internal MyAssert condition =
            if not condition then
                failwith "Assertion failed."
    
    /// Type of player
    type Player = Computer | Human with
        member this.Invert =
            match this with
                | Computer -> Human
                | Human -> Computer

    /// Sides of the Earth
    type SideOfEarth =    
        | North
        | East
        | South
        | West
        
    type public GameState =
        /// The game is on.
        | Default
        /// The game is over. The winner is the following.
        | Won of Player
        /// The game is over. Nobody won.
        | Tie
    
    /// Player's moving direction.
    /// Human's goal is always on south. Computer's goal is always at north.
    /// So human tries to go north, and computer tries to go south.
    type Direction =
        | Simple of SideOfEarth
        | Mix of SideOfEarth * SideOfEarth with
        static member internal All : Direction list =
            [Simple North; Mix (North, East); Simple East; Mix (South, East); Simple South; Mix (South, West); Simple West; Mix (North, West)]

    type ISpot =
        abstract X : int
        abstract Y: int
    /// A point at the game field.
    type Spot = {X: int; Y: int} with
        /// Evaluates the coordinates of the spot shifted to the specified side of earth.
        //[<OverloadID("Shift.SideOfEarth")>]
        member private this.Shift (soe: SideOfEarth) =
            match soe with
                | North -> { this with Y = this.Y + 1 }
                | East -> { this with X = this.X + 1 }
                | South -> { this with Y = this.Y - 1 }
                | West -> { this with X = this.X - 1 }
        /// Evaluates the coordinates of the spot shifted to the specified direction.
        //[<OverloadID("Shift.Direction")>]
        member (*internal*) public this.Shift (dir: Direction) =
            match dir with
                | Simple some -> this.Shift some
                | Mix (some1, some2) -> (this.Shift some1).Shift some2
        /// Evaluates the coordinates of the spot sequentally shifted by the specified directions.
        member internal this.Shift (dirs: seq<Direction>) =
            Seq.fold (fun (state:Spot) (next:Direction) -> state.Shift next) this dirs
        interface ISpot with
            member this.X
                with get () = this.X
            member this.Y
                with get () = this.Y

    /// Kind of the state of a move.
    type (*internal*) MoveState =
        /// Previous move has been completed, next move has not yet been started.
        | Ready
        /// Current position is in the tie place.
        | Tie
        /// The game has not yet been started.
        | Start
        /// Game finished. Somebody won.
        | Win
        /// The move has been started, but has not yet been completed. Current position is on some old line.
        | Incomplete

    [<AbstractClass>]
    type IMatch() =
        abstract CurrentPosition : Spot

        abstract CanMakeMove : seq<Direction> * Spot * Spot -> bool

        abstract CurrentMoveState: seq<Direction> -> MoveState
        member this.CurrentMoveState() =
            this.CurrentMoveState Seq.empty

    /// Generic game engine. Implements computer's behaviour in the game.
    [<AbstractClass>]
    type internal MoverBase(game: IMatch) =
        abstract member MakeMove: unit -> Direction list option
    
    /// Simple game engine. A stub for testing purposes.
    type internal RandomMover(game: IMatch) =
        inherit MoverBase(game)
        let fromIntToDir = function
            | 0 -> Simple North
            | 1 -> Mix (North, East)
            | 2 -> Simple East
            | 3 -> Mix (South, East)
            | 4 -> Simple South
            | 5 -> Mix (South, West)
            | 6 -> Simple West
            | 7 -> Mix (North, West)
            | _ -> failwith "Argument is out of range."
        let rnd = new Random()
        override this.MakeMove () =
            let rec getMove (position: Spot) (invalidDirections: Direction list) (goneDirections: Direction list) =
                if invalidDirections.Length = Direction.All.Length then
                    failwith "Computer cannot find a move." // DEBUG //
                    None
                elif (goneDirections.Length > 0) && ((goneDirections |> List.ofSeq |> List.rev |> Seq.ofList |> game.CurrentMoveState) <> MoveState.Incomplete) then
                    goneDirections |> List.ofSeq |> List.rev |> Some
                else
                    let rec loopGetRnd () =
                        let nextDir = fromIntToDir (rnd.Next(8))
                        if List.exists (fun item -> item = nextDir) invalidDirections then
                            loopGetRnd ()
                        else
                            nextDir
                    let nextDir = loopGetRnd ()
                    
                    let sourceSpot = position
                    let targetSpot = sourceSpot.Shift nextDir
                    if game.CanMakeMove(goneDirections |> List.ofSeq |> List.rev |> Seq.ofList, sourceSpot, targetSpot) then
                        //Some (seq [nextDir])
                        getMove (position.Shift nextDir) [](*invalidDirections*) (nextDir::goneDirections)
                    else
                        getMove position (nextDir :: invalidDirections) goneDirections
            //let result = getMove game.CurrentPosition [] []
            //Some (result.Value :> seq<Direction>)
            //result
            getMove game.CurrentPosition [] []
    
    /// Style of game. Determines the behaviour of computer-player.
    type GameStyle = Random | Smart
    
    /// Represents a "Squared Soccer" game between the computer and a human player.
    type public Match(xSize: int, ySize: int, isComputerTurn: bool, gameStyle: GameStyle) =
        inherit IMatch()
        let whoseTurn = ref ( if isComputerTurn then Computer else Human )
        let gameState = ref Default
        let initialPosition = { X = 0; Y = 0}
        let changeTurn () =
            whoseTurn := (!whoseTurn).Invert
        let moves = new System.Collections.Generic.List< Spot * Spot >()
        let currentPosition () =
            if moves.Count = 0 then
                initialPosition
            else
                snd moves.[moves.Count - 1]
        let brain = ref None
        let compWonPosition = {X = 0; Y = -ySize}
        let humanWonPosition = {X = 0; Y = ySize}
        /// this object is responsible for computer's AI
        member private this.Brain
            with get () =
                if (!brain).IsNone then
                    brain :=
                        match gameStyle with
                            | GameStyle.Random -> Some (new RandomMover(this))
                            | _ -> failwith "Unsupported game style."
                (!brain).Value
        /// Determines if a move is possible from the current position considering a sequence of virtual steps (dirs parameter).
        member private this.isMovePossible (dirs: seq<Direction>)(* (curPos: Spot)*) =
            let curPos_ = this.CurrentPosition.Shift dirs
            Direction.All |>
            Seq.map (fun dir -> (curPos_, curPos_.Shift dir)) |>
            Seq.exists (fun pair -> this.CanMakeMove(dirs, fst pair, snd pair))
        member private this.updateState () =
            let curPos = currentPosition ()
            if curPos = compWonPosition then
                gameState := GameState.Won Computer
            elif curPos = humanWonPosition then
                gameState := GameState.Won Human
            elif this.isMovePossible Seq.empty then
                gameState := GameState.Default
            else
                gameState := GameState.Tie
        /// Performs a simple single-square move.
        member private this.MakeSimpleMove (dir: Direction) : bool =
            let source = currentPosition ()
            let target = source.Shift dir
            if (this :> IMatch).CanMakeMove(Seq.empty, source, target) then
                moves.Add (source, target)
                this.updateState ()
                true
            else
                false
        /// Gets current state of the game.
        member public this.GameState = !gameState
        /// Gets the values indicating whose the next turn is.
        member public this.WhoseTurn = !whoseTurn
        /// Makes human's move. Returns the values indicating if the move completed.
        member public this.MakeHumanMove (dirs: seq<Direction>) : bool =
//            assert (this.WhoseTurn = Human)
            Utils.MyAssert (this.WhoseTurn = Human)
//            assert (this.GameState = Default)
            Utils.MyAssert (this.GameState = Default)
//            let rec tryMove (doneMoves: (Spot * Spot) list) (curPos: Spot) (directions: Direction list) =
//                if doneMoves.Length = directions.Length then
//                    doneMoves
//                else
//                    let nextDirection = directions.[doneMoves.Length]
//                    let source = curPos
//                    let target = curPos.Shift nextDirection
//                    if (this :> IMatch).CanMakeMove (source, target) &&
//                        (not (List.exists (fun (i: Spot * Spot) -> (fst i = source) && (snd i = target)) doneMoves)) &&
//                        (not (List.exists (fun (i: Spot * Spot) -> (fst i = target) && (snd i = source)) doneMoves)) then
//                        tryMove ((source, target)::doneMoves) target directions
//                    else
//                        doneMoves
            let listOfDirections = List.ofSeq dirs
//            let permittedMoves = tryMove (List.empty) ((this :> IMatch).CurrentPosition) listOfDirections |> List.rev
//            if permittedMoves.Length = listOfDirections.Length then
//                moves.AddRange permittedMoves
//                changeTurn()
//                true
//            else
//                false            
            let i = ref 0
            let success = ref true
            while (!i < listOfDirections.Length) && (!success) && (this.GameState = Default) do
                let moveState = this.CurrentMoveState(Seq.empty)
                let startMove = (!i = 0) && (moveState = MoveState.Ready || moveState = MoveState.Start)
                let continueMove = (!i > 0) && (moveState = MoveState.Incomplete)
                let correctMove = startMove || continueMove
                if correctMove && this.MakeSimpleMove listOfDirections.[!i] then
                    i := !i + 1
                else
                    success := false
            if !success && (this.CurrentMoveState() <> MoveState.Incomplete) then
                changeTurn()
                true
            else
                if !i > 0 then
                    moves.RemoveRange (moves.Count - !i, !i)
                false
        /// Makes one step of human's move. Returns the values indicating if the step completed.
        member public this.MakeHumanMovePartial (dir:Direction) : bool =
//            assert (this.WhoseTurn = Human)
            Utils.MyAssert (this.WhoseTurn = Human)
//            assert (this.GameState = Default)
            Utils.MyAssert (this.GameState = Default)
            let result = this.MakeSimpleMove dir
            if result && this.CurrentMoveState() <> MoveState.Incomplete then
                changeTurn()
            result
        /// Gets next computer's move as a sequence of moves.
        member public this.GetComputerMove () =
//            assert (this.WhoseTurn = Computer)
            Utils.MyAssert (this.WhoseTurn = Computer)
//            assert (this.GameState = Default)
            Utils.MyAssert (this.GameState = Default)
            let result = this.Brain.MakeMove ()
            if result.IsSome then
                // 1st implementation
                //Seq.iter (fun dir -> assert (this.MakeSimpleMove dir)) result.Value
                // 2nd implementation
                //Seq.exists (fun dir -> (this.GameState <> Default) && (not (this.MakeSimpleMove dir))) result.Value
                // 3rd implementation
                //let success = ref true
                //for dir in result.Value do
                //    if (!success) && (this.GameState = Default) && (not (this.MakeSimpleMove dir)) then
                //        success := false
                //changeTurn()
                // 4th implementation
//                let firstMove = ref true
//                for dir in result.Value do
//                    // check if the move state is correct
//                    let moveState = this.CurrentMoveState(Seq.empty)
//                    let startMove = !firstMove && (moveState = MoveState.Start || moveState = MoveState.Ready)
//                    let continueMove = (not !firstMove) && (moveState = MoveState.Incomplete)
//                    assert (startMove || continueMove)
//                    // make the move and check if it has completed successfully
//                    assert ((this.GameState = Default) && (this.MakeSimpleMove dir))
//                    firstMove := false
                // 5th implementation
                let checkAndMove i dir =
                    // check if the move state is correct
                    let moveState = this.CurrentMoveState(Seq.empty)
                    let startMove = (i = 0) && (moveState = MoveState.Start || moveState = MoveState.Ready)
                    let continueMove = (i > 0) && (moveState = MoveState.Incomplete)
                    //assert (startMove || continueMove)
                    //Utils.MyAssert (startMove || continueMove)
                    if not (startMove || continueMove) then
                        failwith "Incorrect step."
                    
                    // make the move and check if it has completed successfully
                    //assert ((this.GameState = Default) && (this.MakeSimpleMove dir))
                    //Utils.MyAssert (this.GameState = Default)
                    if not (this.GameState = Default) then
                        failwith "Incorrect game state."
                    //Utils.MyAssert (this.MakeSimpleMove dir)
                    if not (this.MakeSimpleMove dir) then
                        failwith "Failed to make simple move."
                Seq.iteri checkAndMove result.Value
                // DEBUG
//                let i = ref 0
//                for dir in result.Value do
//                    // check if the move state is correct
//                    let moveState = this.CurrentMoveState(Seq.empty)
//                    let startMove = (!i = 0) && (moveState = MoveState.Start || moveState = MoveState.Ready)
//                    let continueMove = (!i > 0) && (moveState = MoveState.Incomplete)
////                    assert (startMove || continueMove)
//                    //Utils.MyAssert (startMove || continueMove)
//                    if not (startMove || continueMove) then
//                        failwith "Incorrect step."
//                    // make the move and check if it has completed successfully
////                    assert ((this.GameState = Default) && (this.MakeSimpleMove dir))
//                    //Utils.MyAssert (this.GameState = Default)
//                    if not (this.GameState = Default) then
//                        failwith "Incorrect game state."
//                    //Utils.MyAssert (this.MakeSimpleMove dir)
//                    if not (this.MakeSimpleMove dir) then
//                        failwith "Failed to make simple move."
//                    i := !i + 1
                // DEBUG
                //assert (this.CurrentMoveState() <> MoveState.Incomplete)
                Utils.MyAssert (this.CurrentMoveState() <> MoveState.Incomplete)
                changeTurn ()
            else
                // TODO: think about it... 
                //gameState := GameState.Tie
                failwith "Computer cannot make a move."
            result
        //interface IMatch with
        /// Gets current ball position on the field.
        override this.CurrentPosition
            with get () = currentPosition ()
        /// Determines whether the specified move can be performed.
        override this.CanMakeMove (dirs: seq<Direction>, src: Spot, trg: Spot) =
            let newMoves =
                dirs
                |> Seq.scan (fun (state: Spot * Spot) next -> (snd state, (snd state).Shift next)) (this.CurrentPosition, this.CurrentPosition)
                |> Seq.skip 1
            let conflictOldMove =
                moves.Contains (src, trg) ||
                moves.Contains (trg, src) ||
                Seq.exists (fun pair -> (fst pair = src && snd pair = trg) || (fst pair = trg && snd pair = src)) newMoves
            let conflistMiddleLine = (src.Y = 0) && (trg.Y = 0)
            let conflistBorder =
                ((src.X = trg.X) && (src.X = -xSize || src.X = xSize))  // left or right border
                || ((src.Y = trg.Y) && (src.Y = -ySize || src.Y = ySize))   // bottom or top border
            let outOfScope = (xSize < Math.Abs trg.X) || (ySize < Math.Abs trg.Y)
            not (outOfScope || conflictOldMove || conflistMiddleLine || conflistBorder)
        /// Returns the state of current move - completed or not.
        override this.CurrentMoveState dirs =
            let curPos = this.CurrentPosition.Shift dirs
//            let newMoves =
//                dirs
//                |> Seq.scan (fun (state: Spot * Spot) next -> (snd state, (snd state).Shift next)) (this.CurrentPosition, this.CurrentPosition)
//                |> Seq.skip 1
//                |> Seq.toList                
            let newMoves =
                dirs
                |> List.ofSeq
                //|> List.rev
                |> List.scan (fun (state: Spot * Spot) next -> (snd state, (snd state).Shift next)) (this.CurrentPosition, this.CurrentPosition)
                |> List.tail
            let isOnPrevMove =
                let matchingEdge = fun move -> (fst move = curPos) || (snd move = curPos)
                match dirs with
                    | x when x = Seq.empty -> (moves.Count > 0) && (moves.FindIndex(0, moves.Count - 1, fun move -> matchingEdge move) > -1)
                    | _ -> (moves.Exists (fun move -> matchingEdge move)) || (newMoves |> List.rev |> List.tail |> List.exists matchingEdge)
            let isOnMiddleLine = (curPos.Y = 0)
            let isOnBorder = (curPos.X = -xSize) || (curPos.X = xSize) || (curPos.Y = -ySize) || (curPos.Y = ySize)
            // DEBUG
//            let my_isOnPrev1 = ref false
//            for i=0 to moves.Count-2 do
//                if (fst moves.[i] = curPos) || (snd moves.[i] = curPos) then
//                    my_isOnPrev1 := true;
//
//            let my_isOnPrev2 = ref false
//            for j=0 to newMoves.Length-2 do
//                if (fst newMoves.[j] = curPos) || (snd newMoves.[j] = curPos) then
//                    my_isOnPrev2 := true
//
//            if not ((!my_isOnPrev1 || !my_isOnPrev2) = isOnPrevMove) then
//                failwith "AGA!"
            // DEBUG
            if moves.Count = 0 && Seq.isEmpty dirs then
                MoveState.Start
            elif curPos = compWonPosition || curPos = humanWonPosition then
                MoveState.Win
            elif not (this.isMovePossible dirs) then
                MoveState.Tie
            elif isOnMiddleLine || isOnBorder || isOnPrevMove then
                MoveState.Incomplete
            else
                MoveState.Ready
